package logs

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"time"

	ckeadmapi "ckeadm/cmd/apis"
	"ckeadm/cmd/runner"
	"ckeadm/constants"
	"ckeadm/util"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

var (
	followLog  = logrus.New() // fllow create, show when create, show always set
	serviceLog = logrus.New() // run watch as service, show service logs
)

// showld set in cluster.yml
func WaitCommand() cli.Command {
	var waitFlags = []cli.Flag{
		runner.ConfigPathFlag,
		runner.LogLevelFlag,
		runner.CacheDirFlag,
		runner.AddressFlag,
		runner.ClusterIdFlag,
		runner.WatchPortFlag,
		runner.FollowingFlag,
		runner.PrivateKey,
		runner.AddressListFlag,
	}

	return cli.Command{
		Name:   "wait",
		Usage:  "Bring the cluster wait for information",
		Action: runWait,
		Flags:  waitFlags,
		Hidden: true, // hidden this, use in create command
	}
}

/*
TODO: follow and unfollow. now new unfollow just, add follow if have time
* follow: create and run, show informations about install
* unfollow: create and run backend, use service listen for progress information
*/
//following = false
func runWait(ctx *cli.Context) error {
	// ======================================== debug ========================================
	for _, value := range ctx.Command.Flags {
		logrus.Infof("recognized and used variable: %s = %s", value.GetName(), ctx.String(value.GetName()))
	}
	// ======================================== debug ========================================

	var MainLogLevel, SecondLogLevel = logrus.Level(ctx.Int("v")), logrus.PanicLevel
	serviceLog.SetLevel(MainLogLevel)
	followLog.SetLevel(SecondLogLevel)
	if ctx.Bool("following") {
		serviceLog.SetLevel(SecondLogLevel)
		followLog.SetLevel(MainLogLevel)
	}

	fmt.Printf("\n\n")
	logrus.Infof("service logs level: %s, follow logs level: %s\n", serviceLog.GetLevel(), followLog.GetLevel())

	//var clusterId string
	//if ctx.String("f") == EmptyString {
	//	clusterId = ctx.String("clusterID")
	//}else {
	//	if config, err := runner.GetConfigFromFile(ctx.String("f")); err == nil {
	//		clusterId = config.ClusterId
	//	}else {
	//		return fmt.Errorf("fail to get clusterId, err %s", err.Error())
	//	}
	//}

	//add go routine to process log sync
	//use channel to deliver logs
	logChan := make(chan ckeadmapi.LogRoutineParam, 10000)
	go SyncLogForNodesRoutine(logChan)

	http.HandleFunc(constants.CkeadmWaitInfoPattern, HandleSetProgressWithPerm(constants.EmptyString, ctx.String("cachedir")))
	http.HandleFunc(constants.CkeadmWaitProgressPattern, HandleGetProgress(constants.AnsibleUpDoneFile, ctx.String("cachedir"))) // just create
	http.HandleFunc(constants.CkeadmWaitHealthzPattern, HandleHealthCheck)
	http.HandleFunc(constants.CkeadmWaitLogPattern, HandleLogCollect(ctx.String("address"), ctx.String("address-list"), ctx.String("private-key"), logChan))

	serviceLog.Infof("trying to listening and service on: %s:%d\n", ctx.String("address"), ctx.Int("port"))
	return http.ListenAndServe(fmt.Sprintf("%s:%d", ctx.String("address"), ctx.Int("port")), nil)
}

func HandleHealthCheck(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("true"))
}

func HandleSetProgressWithPerm(clusterID, cacheDir string) func(w http.ResponseWriter, r *http.Request) {
	// result as json
	// {
	//   "clusterID": "cluster_for_app",
	//   "nodeName": "master1",
	//   "DoneFile": "up.done",
	//   "DoneContext": "xxxxxxxxxxxxxx"
	// }
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		var Result ckeadmapi.LogContext
		context, err := ioutil.ReadAll(r.Body)
		defer r.Body.Close()
		if err = json.Unmarshal(context, &Result); err != nil {
			serviceLog.Warningf("refuse connect from %s, err: %s", r.RemoteAddr, err.Error())
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		serviceLog.Infof("cluster %s node %s, file %s, get info %s\n", Result.ClusterID, Result.NodeName, Result.DoneFile, strings.ReplaceAll(Result.DoneInfo, "\n", "\\n"))

		//if Result.ClusterID == clusterID {
		//	nodeRunningInfo := runner.GetNodeInstallAllInfo(constants.CkectlUpPhaseNum + 1, ckeadmapi.CkeConfigNode{Name: Result.NodeName}, strings.Split(Result.DoneFile, "\n"), true, EmptyString)
		//	fllowLog.Infof("node %s has complate %s, get info %s\n", nodeRunningInfo.Name, nodeRunningInfo.Progress, nodeRunningInfo.Message)
		//}

		_ = util.WriteFile(fmt.Sprintf("%s/%s/%s.%s", cacheDir, Result.ClusterID, Result.NodeName, Result.DoneFile), Result.DoneInfo)
	}
}

// example: curl genssis01:{{.port}}/progress/cluster_for_app?v=4
func HandleGetProgress(DoneFile string, cacheDir string) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		if !regexp.MustCompile(fmt.Sprintf("%s.*", constants.CkeadmWaitProgressPattern)).MatchString(r.URL.Path) {
			serviceLog.Errorf("fail to get cluster id from request path %s\n", r.URL.Path)
		}
		clusterId := strings.Split(r.URL.Path, "/")[2]
		serviceLog.Infof("handle get progress information from %s, cluster id: %s\n", r.RemoteAddr, clusterId)

		configPath := fmt.Sprintf("%s/%s/%s", constants.CkeDeployConfigPath, clusterId, constants.SourcesFile)
		if _, err := os.Stat(configPath); err != nil {
			serviceLog.Errorf("remote %s, clusterId not found %s\n", r.RemoteAddr, clusterId)
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte(fmt.Sprintln("No resources found in this ClusterID.")))
			return
		}

		var set = &flag.FlagSet{}
		var ctx = cli.NewContext(nil, set, nil)
		set.String("cachedir", cacheDir, "cache directory for logs")
		set.String("f", configPath, "configration file path")

		buff := bytes.NewBuffer([]byte{})
		if err := runner.CkeadmLogsRunner(ctx, buff, DoneFile, GetInstallMsgsFromLocal(ctx)); err != nil {
			serviceLog.Errorf("remote %s, err %s\n", r.RemoteAddr, err.Error())
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte(err.Error()))
			return
		}

		info, _ := ioutil.ReadAll(buff)
		_, _ = w.Write(info)
	}
}

func HandleLogCollect(address, addressList, privateKey string, logQueue chan ckeadmapi.LogRoutineParam) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		addressArray := strings.Split(addressList, ",")
		for index, item := range addressArray {
			if item == address {
				addressArray = append(addressArray[:index], addressArray[index+1:]...)
			}
		}
		var componentLogArray []ckeadmapi.ComponentLog
		context, err := ioutil.ReadAll(r.Body)
		defer r.Body.Close()
		if err = json.Unmarshal(context, &componentLogArray); err != nil {
			serviceLog.Warningf("refuse connect from %s, err: %s", r.RemoteAddr, err.Error())
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		timeStr := time.Now().Format("20060102")
		for _, log := range componentLogArray {
			serviceLog.Infof("cluster %s node %s, component %s, action %s, logId %d, time %s, logContent %s\n",
				log.ClusterID, log.NodeName, log.ComponentName, log.Action, log.LogId, log.TimeStamp, strings.ReplaceAll(log.LogContent, "\n", "\\n"))
			//append log file
			logFileName := fmt.Sprintf("%s_%s_%s_%s.log", log.ComponentName, log.NodeName, timeStr, log.Suffix)
			dirPath := fmt.Sprintf("%s/%s/%s/%s", constants.CkeComponentLogPath, log.ClusterID, log.ComponentName, log.Action)
			_ = os.MkdirAll(dirPath, 0644)
			err = util.AppendFile(fmt.Sprintf("%s/%s", dirPath, logFileName),
				fmt.Sprintf("%s [%d] : %s", log.TimeStamp, log.LogId, strings.ReplaceAll(log.LogContent, "\n", "\\n")))
			if err != nil {
				serviceLog.Errorf("failed to write log file: %s", err.Error())
			}

		}
		//send to chan
		logQueue <- ckeadmapi.LogRoutineParam{ComponentLogArray: componentLogArray, AddressArray: addressArray, KeyFile: privateKey, Time: timeStr}
	}
}

//收到的日志数组内容一定属于同一个组件
func SyncLogForNodesRoutine(logQueue chan ckeadmapi.LogRoutineParam) {
	var wg sync.WaitGroup
	for {
		select {
		case logRoutineParam := <-logQueue:
			fmt.Printf("log chan received, %v \n", logRoutineParam)
			for _, ip := range logRoutineParam.AddressArray {
				if len(logRoutineParam.ComponentLogArray) >= 1 {
					wg.Add(1)
					go func(logRoutineParam ckeadmapi.LogRoutineParam, ip string) {
						defer wg.Done()
						firstLog := logRoutineParam.ComponentLogArray[0]
						//mkdir
						dirPath := fmt.Sprintf("%s/%s/%s/%s", constants.CkeComponentLogPath, firstLog.ClusterID, firstLog.ComponentName, firstLog.Action)
						dirCmd := fmt.Sprintf("mkdir -p %s", dirPath)
						//fmt.Printf("SSH 1 begin at %s \n", time.Now().String())
						stdoutChan, stderrChan, doneChan, errChan, err := util.SetupSSH(ip, logRoutineParam.KeyFile).Stream(dirCmd, 600*time.Second)
						//fmt.Printf("now is %s \n", time.Now().String())
						if err != nil {
							logrus.Errorf("dir %s create failed, err: %s", dirPath, err.Error())
						} else {
							// read from the output channel until the done signal is passed
							isTimeout := true
						loop1:
							for {
								select {
								case isTimeout = <-doneChan:
									break loop1
								case outline := <-stdoutChan:
									fmt.Printf("outline now is %s \n", time.Now().String())
									logrus.Info("out:", outline)
								case errline := <-stderrChan:
									fmt.Printf("errline now is %s \n", time.Now().String())
									logrus.Info("out:", errline)
								case err = <-errChan:
								}
							}

							// get exit code or command error.
							if err != nil {
								fmt.Println("err: " + err.Error())
							}

							// command time out
							if !isTimeout {
								logrus.Errorf("SyncLogForNodes, err: mkdir command timeout")
							}
						}
						logFileName := fmt.Sprintf("%s/%s/%s/%s/%s_%s_%s_%s.log", constants.CkeComponentLogPath, firstLog.ClusterID, firstLog.ComponentName, firstLog.Action,
							firstLog.ComponentName, firstLog.NodeName, logRoutineParam.Time, firstLog.Suffix)
						//fmt.Printf("SCP begin at %s \n", time.Now().String())
						scpCmd := fmt.Sprintf("scp -o StrictHostKeyChecking=no -i %s -r %s %s:%s", logRoutineParam.KeyFile,
							logFileName, ip, logFileName)
						// #nosec
						cmd := exec.Command("sh", "-c", scpCmd)
						output, err := cmd.CombinedOutput()
						logrus.Infof("scp output: %s", string(output))
						if err != nil {
							logrus.Errorf("execute scp log file cmd failed: %s", err.Error())
						}
					}(logRoutineParam, ip)
				}
			}
			wg.Wait()
		}
	}
}
